组件化是 Vue 的一大核心,接下来看看组件化的源码实现。
以下面例子进行分析:
<!-- App.vue -->
<template>
<div id="app"></div>
</template>
// main.js
import Vue from 'vue';
import App from './App.vue';
new Vue({
el: '#app',
render: h => h(App)
});
在new Vue
进行实例化 Vue 之后,会执行 $mount 函数,实际上就是执行 mountComponent 函数,该函数会实例化一个渲染 watcher,在渲染 watcher 实例化过程中,会执行其传入的 updateComponent 函数。updateComponent 函数源码如下:
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
_render 函数
:生成 vnode。_update 函数
:执行 patch 逻辑,根据 vnode 生成并挂在实际的 DOM。
# _render
由于 _render
最后调用的是 _createElement 函数,直接分析该函数。上面例子由于 render 函数传入的是 vue 组件,所以在 _createElement 函数的执行逻辑为调用 createComponent 函数:
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
// ...
if (typeof tag === 'string') {
// ...
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 执行 createComponent 函数生成组件 vnode
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// ...
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
// ...
}
先来看生成组件 vnode 的 createComponent 函数源码逻辑。
# createComponent 函数
function createComponent (
Ctor,
data,
context,
children,
tag
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
// 将 vue 组件的 options 转化为一个构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
{
warn(("Invalid Component definition: " + (String(Ctor))), context);
}
return
}
// 异步组件处理逻辑
var asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {};
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor);
// v-model 语法转化为 props 和 events 形式
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
// props 处理
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
// 函数式组件处理
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}
// 安装组件钩子
installComponentHooks(data);
// 生成并返回一个占位的 vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
上面的源码,先着重看几个,其他的后面再看。
// 将 vue 组件的 options 转化为一个构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// 安装组件钩子
installComponentHooks(data);
// 生成并返回一个占位的 vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
# extend 函数
extend 函数在引入 vue 的时候就挂载到 vue 上了。实现逻辑在:initGlobalAPI 函数 -> initExtend 函数
。
function initExtend (Vue) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0;
var cid = 1;
/**
* Class inheritance
*/
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name);
}
// 定义组件构造器
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
// 合并配置项目,使当前组件拥有全局配置
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
// 初始化 props
if (Sub.options.props) {
initProps$1(Sub);
}
// 初始化 computed
if (Sub.options.computed) {
initComputed$1(Sub);
}
// 继承静态方法
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// 缓存构造器
cachedCtors[SuperId] = Sub;
return Sub
};
}
该函数主要通过函数构造器创建 Vue 的子类,然后通过继承的方式将一个对象转化为一个继承于 Vue 的构造器 Sub 并返回。同时,对 Sub 本身也做了一些处理,如:props 和 computed 初始化,合并配置项目使当前组件拥有全局配置,继承静态方法等。
# installComponentHooks 函数
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
prepatch: function prepatch (oldVnode, vnode) {
// ...
},
insert: function insert (vnode) {
// ...
},
destroy: function destroy (vnode) {
// ...
}
}
var hooksToMerge = Object.keys(componentVNodeHooks);
function installComponentHooks (data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
}
}
}
function mergeHook$1 (f1, f2) {
var merged = function (a, b) {
// flow complains about extra args which is why we use any
f1(a, b);
f2(a, b);
};
merged._merged = true;
return merged
}
该函数主要讲 componentVNodeHooks 的 hook 合并到 vm.data.hook 中,使每个 vue 实例的 vm.data.hook 能执行对应的钩子。
# _update
回到最开始的例子,在通过 createComponent 生成 App 的 vnode 之后,接下来就是通过 _update
函数进行 patch 的逻辑。由于 render 函数传入的是 vue 组件,所以执行逻辑为:
function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// 旧 vnode 为空,直接根据新 vnode 创建元素节点
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
}
// ...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
# createComponent 函数
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
// 调用 init hook
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// 在调用了 init hook 之后,在上面例子中,由于 render 传入的是一个 组件,所以会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,然后调用 insert 将 vnode.elm 插入到父节点中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
该函数主要完成一下几件事:调用 init hook,创建组件实例;在执行完 init hook 之后,在最开始例子中,由于 render 传入的是一个 组件,所以会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,最后调用 insert 将 vnode.elm 插入到父节点中。
# _init 组件钩子
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// keep-alive 组件逻辑
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 普通组件逻辑
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
}
}
init 函数主要有两个逻辑,一个是 keep-alive 组件,一个是普通组件。主要看普通组件逻辑,它会先执行 createComponentInstanceForVnode 函数创建组件实例并赋值给 vnode.componentInstance,然后执行 $mount
进行挂载,由于当前组件的 vnode 的 elm 为 空,所以 $mount
的第一个参数为 undefined。
# createComponentInstanceForVnode 函数
该函数有两个参数:
- vnode:当前组件的 vnode
- activeInstance:当前激活的 vue 实例。在我们例子中也就是当前 app 组件的父组件上下文 vm,也就是
new Vue
。
activeInstance 当前实例逻辑在 _update 最开始执行:
var activeInstance = null; // 全局变量 activeInstance
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// ...
}
function setActiveInstance(vm) {
var prevActiveInstance = activeInstance;
activeInstance = vm; // 将 activeInstance 设置为当前上下文 vm
return function () {
// 恢复上一个上下文
activeInstance = prevActiveInstance;
}
}
下面是 createComponentInstanceForVnode 函数源码:
function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode,
// activeInstance in lifecycle state
parent
) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
该函数先检查是否是内联 template render 函数,是的话执行对应逻辑。在最开始例子中,由于不是内联 template,所以直接执行并返回 new vnode.componentOptions.Ctor(options)
。这个执行逻辑实际上就是执行:在 _render 时通过 createComponent 生成的组件构造器。
function initExtend (Vue) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0;
var cid = 1;
/**
* Class inheritance
*/
Vue.extend = function (extendOptions) {
// ...
// 定义组件构造器
var Sub = function VueComponent (options) {
this._init(options);
};
// ..
return Sub
};
}
接下来执行 _init 逻辑,下面是组件类型的执行逻辑源码:
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
// ...
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
/* istanbul ignore if */
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
// 由于是子组件类型,el 为 undefined
if (vm.$options.el) {
// 这个挂载逻辑不会在这里执行
vm.$mount(vm.$options.el);
}
};
}
主要关注两点:
- initInternalComponent 函数:合并组件的配置项。
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
- 由于是子组件类型,el 为 undefined,$mount 组件挂载逻辑不会在这里执行。
回到 componentVNodeHooks init 钩子,执行完 createComponentInstanceForVnode 函数生成组件实例后,会执行 $mount 进行挂载。
组件挂载的逻辑流程跟之前一样,生成渲染 watcher,然后通过 _render 函数生成 vnode,最后通过 _update 函数执行 patch 逻辑得到实际的 DOM 节点 vnode.elm 。在 patch 逻辑中,会执行到的主要源码如下:
function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// 旧 vnode 为空,直接根据新 vnode 创建元素节点
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
}
// ...
return vnode.elm
}
执行完 _update 之后,init hook 逻辑也就执行完了。在最开始例子中,接着会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,最后调用 insert 将 vnode.elm 插入到父节点中。
initComponent 函数源码如下:
function initComponent(vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
// 真实的 DOM 赋值给 vnode.elm
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
// 执行在 patch 函数中初始化过的 create hook,存放在变量 cbs 中
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode);
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}
# 总结
Vue 组件化渲染的过程是一个深度优先遍历的过程。渲染过程如果遇到子组件会先将该子组件通过 _render 创建子组件构造器和安装组件钩子,然后通过 _update 创建子组件实例并得到组件的真实 DOM 节点,最后将真实的 DOM 挂载到其父节点上。